Skip to content

Commit a074182

Browse files
committed
add bot handling
1 parent 40fbc25 commit a074182

File tree

13 files changed

+88
-10
lines changed

13 files changed

+88
-10
lines changed

kitsune/community/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def top_contributors_l10n(
145145

146146
users = (
147147
User.objects.filter(created_revisions__in=revisions, is_active=True)
148+
.exclude(profile__is_bot=True)
148149
.annotate(query_count=Count("created_revisions"))
149150
.order_by(F("query_count").desc(nulls_last=True))
150151
.select_related("profile")

kitsune/l10n/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def get_l10n_bot():
7979
username="sumo-l10n-bot", defaults=dict(email="sumodev@mozilla.com")
8080
)
8181
if created:
82-
Profile.objects.create(user=user, name="SUMO Localization Bot")
82+
Profile.objects.create(user=user, name="SUMO Localization Bot", is_bot=True)
8383
return user
8484

8585

kitsune/sumo/form_fields.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,14 @@ def to_python(self, value):
106106
to_objects = {}
107107
for key, value in key_value_pairs:
108108
# check if the value is a valid username in the database
109-
if key.lower() == "user" and not User.objects.filter(username=value).exists():
110-
raise ValidationError(_("{name} is not a valid username.").format(name=value))
109+
if key.lower() == "user":
110+
if not User.objects.filter(username=value).exists():
111+
raise ValidationError(_(f"{value} is not a valid username."))
112+
if User.objects.filter(username=value, profile__is_bot=True).exists():
113+
raise ValidationError(
114+
_(f"{value} is a bot. You cannot send messages to bots.")
115+
)
116+
111117
to_objects.setdefault(f"{key.lower()}s", []).append(value)
112118

113119
return to_objects

kitsune/sumo/static/sumo/scss/layout/_contributors.scss

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@
7474
width: 100%;
7575
max-width: none;
7676
margin: 0 0 p.$spacing-md 0;
77+
78+
&[type="checkbox"] {
79+
width: auto;
80+
margin-right: p.$spacing-md;
81+
margin-left: p.$spacing-xs;
82+
}
7783
}
7884

7985
img {
@@ -86,7 +92,6 @@
8692
select,
8793
input {
8894
margin: 0 10px;
89-
9095
}
9196
}
9297
}
@@ -263,4 +268,4 @@ ol.headings-list {
263268
}
264269
}
265270
}
266-
}
271+
}

kitsune/users/admin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class ProfileAdmin(admin.ModelAdmin):
4242
"fxa_refresh_token",
4343
"zendesk_id",
4444
"fxa_avatar",
45+
"is_bot",
4546
],
4647
},
4748
),
@@ -79,7 +80,7 @@ class ProfileAdmin(admin.ModelAdmin):
7980
list_filter = ["is_fxa_migrated", "country"]
8081
search_fields = ["user__username", "user__email", "name", "fxa_uid"]
8182
autocomplete_fields = ["user"]
82-
readonly_fields = ["fxa_refresh_token", "zendesk_id"]
83+
readonly_fields = ["fxa_refresh_token", "zendesk_id", "is_bot"]
8384

8485
def get_products(self, obj):
8586
"""Get a list of products that a user is subscribed to."""

kitsune/users/api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def usernames(request):
5656

5757
users = (
5858
User.objects.filter(is_active=True)
59+
.exclude(profile__is_bot=True)
5960
.exclude(profile__is_fxa_migrated=False)
6061
.filter(Q(username__istartswith=pre) | Q(profile__name__istartswith=pre))
6162
.select_related("profile")

kitsune/users/jinja2/users/profile.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ <h1 class="sumo-page-heading">{{ _('Your Account') }}</h1>
2727
<hr class="section-break"/>
2828
{% endif %}
2929

30-
{% if request.user.is_authenticated and request.user != profile.user %}
30+
{% if not profile.is_bot and request.user.is_authenticated and (request.user != profile.user) %}
3131
{% if user.has_perm('users.change_profile') %}
3232
<div id="admin-actions">
3333
<a class="edit" rel="nofollow" href="{{ url('users.edit_profile', profile.user.username) }}">
@@ -114,7 +114,7 @@ <h2 class="sumo-page-subheading location">
114114
</section>
115115

116116

117-
{% if user.id != profile.user.id and user.has_perm('users.deactivate_users') %}
117+
{% if not profile.is_bot and (user.id != profile.user.id) and user.has_perm('users.deactivate_users') %}
118118
{% if profile.user.is_active %}
119119
<form class="deactivate" method="post" action="{{ url('users.deactivate') }}">
120120
{% csrf_token %}
@@ -195,6 +195,7 @@ <h2 class="sumo-page-subheading mt-lg">{{ _('Badges') }}</h2>
195195
</section>
196196
{% endif %}
197197

198+
{% if not profile.is_bot %}
198199
<div class="pm-or-signout sumo-button-wrap extra-pad-top">
199200
{% if profile.user == user %}
200201
<form id="sign-out" action="{{ url('users.logout') }}" method="post">
@@ -212,5 +213,6 @@ <h2 class="sumo-page-subheading mt-lg">{{ _('Badges') }}</h2>
212213
{{ private_message(profile.user) }}
213214
{% endif %}
214215
</div>
216+
{% endif %}
215217
</article>
216218
{% endblock %}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.16 on 2024-11-18 14:36
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("users", "0029_remove_profile_avatar"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="profile",
15+
name="is_bot",
16+
field=models.BooleanField(default=False),
17+
),
18+
]

kitsune/users/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class Profile(ModelBase):
5656
user = models.OneToOneField(
5757
User, on_delete=models.CASCADE, primary_key=True, verbose_name=_lazy("User")
5858
)
59+
is_bot = models.BooleanField(default=False)
5960
name = models.CharField(
6061
max_length=255, null=True, blank=True, verbose_name=_lazy("Display name")
6162
)

kitsune/users/utils.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from kitsune.messages.models import InboxMessage, OutboxMessage
1212
from kitsune.sumo import email_utils
1313
from kitsune.tidings.models import Watch
14-
from kitsune.users.models import ContributionAreas, Deactivation, Setting
14+
from kitsune.users.models import ContributionAreas, Deactivation, Profile, Setting
1515

1616
log = logging.getLogger("k.users")
1717

@@ -159,3 +159,8 @@ def user_is_contributor(user):
159159
user.is_authenticated
160160
and user.groups.filter(name__in=ContributionAreas.get_groups()).exists()
161161
)
162+
163+
164+
def user_is_bot(user):
165+
"""Return whether the user is a bot."""
166+
return Profile.objects.filter(user=user, is_bot=True).exists()

kitsune/users/views.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
anonymize_user,
5858
deactivate_user,
5959
get_oidc_fxa_setting,
60+
user_is_bot,
6061
)
6162
from kitsune.wiki.models import user_documents, user_redirects
6263

@@ -146,6 +147,10 @@ def profile(request, username):
146147
@login_required
147148
@require_POST
148149
def close_account(request):
150+
# Forbid this for bots.
151+
if user_is_bot(request.user):
152+
return HttpResponseForbidden()
153+
149154
anonymize_user(request.user)
150155

151156
# Log the user out
@@ -158,6 +163,11 @@ def close_account(request):
158163
@permission_required("users.deactivate_users")
159164
def deactivate(request, mark_spam=False):
160165
user = get_object_or_404(User, id=request.POST["user_id"], is_active=True)
166+
167+
# Forbid this for bots.
168+
if user_is_bot(user):
169+
return HttpResponseForbidden()
170+
161171
deactivate_user(user, request.user)
162172

163173
if mark_spam:
@@ -246,6 +256,10 @@ def documents_contributed(request, username):
246256
@require_http_methods(["GET", "POST"])
247257
def edit_settings(request):
248258
"""Edit user settings"""
259+
# Forbid this for bots.
260+
if user_is_bot(request.user):
261+
return HttpResponseForbidden()
262+
249263
template = "users/edit_settings.html"
250264
if request.method == "POST":
251265
settings_form = SettingsForm(request.POST)
@@ -275,7 +289,11 @@ def edit_settings(request):
275289
@login_required
276290
@require_http_methods(["GET", "POST"])
277291
def edit_contribution_area(request):
278-
"""Edit user settings"""
292+
"""Edit the user's contribution area."""
293+
# Forbid this for bots.
294+
if user_is_bot(request.user):
295+
return HttpResponseForbidden()
296+
279297
template = "users/edit_contributions.html"
280298
contribution_form = ContributionAreaForm(request.POST or None, request=request)
281299

@@ -290,6 +308,10 @@ def edit_contribution_area(request):
290308
@require_http_methods(["GET", "POST"])
291309
def edit_watch_list(request):
292310
"""Edit watch list"""
311+
# Forbid this for bots.
312+
if user_is_bot(request.user):
313+
return HttpResponseForbidden()
314+
293315
watches = Watch.objects.filter(user=request.user).order_by("content_type")
294316

295317
watch_list = []
@@ -328,6 +350,7 @@ def edit_profile(request, username=None):
328350
# Make sure the auth'd user has permission:
329351
if not request.user.has_perm("users.change_profile"):
330352
return HttpResponseForbidden()
353+
331354
if not user:
332355
user = request.user
333356

@@ -338,6 +361,10 @@ def edit_profile(request, username=None):
338361
# a profile. We can remove this fallback.
339362
user_profile = Profile.objects.create(user=user)
340363

364+
# Forbid this for bots.
365+
if user_profile.is_bot:
366+
return HttpResponseForbidden()
367+
341368
profile_form = ProfileForm(request.POST or None, request.FILES or None, instance=user_profile)
342369
user_form = UserForm(request.POST or None, instance=user_profile.user)
343370

@@ -371,6 +398,10 @@ def edit_profile(request, username=None):
371398
@require_http_methods(["POST"])
372399
def make_contributor(request):
373400
"""Adds the logged in user to the contributor group"""
401+
# Forbid this for bots.
402+
if user_is_bot(request.user):
403+
return HttpResponseForbidden()
404+
374405
add_to_contributors(request.user, request.LANGUAGE_CODE)
375406

376407
if "return_to" in request.POST:

kitsune/wiki/forms.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,5 +426,6 @@ class RevisionFilterForm(forms.Form):
426426

427427
locale = forms.ChoiceField(label=_lazy("Locale:"), choices=languages, required=False)
428428
users = MultiUsernameField(label=_lazy("Users:"), required=False)
429+
include_bots = forms.BooleanField(label=_lazy("Bots:"), required=False)
429430
start = forms.DateField(label=_lazy("Start:"), required=False)
430431
end = forms.DateField(label=_lazy("End:"), required=False)

kitsune/wiki/views.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,9 @@ def _show_revision_warning(document, revision):
16941694
def recent_revisions(request):
16951695
request.GET = request.GET.copy()
16961696
fragment = request.GET.pop("fragment", None)
1697+
if not fragment:
1698+
request.GET.setdefault("include_bots", "on")
1699+
16971700
form = RevisionFilterForm(request.GET)
16981701

16991702
# Validate the form to populate cleaned_data, even with invalid usernames.
@@ -1708,6 +1711,9 @@ def recent_revisions(request):
17081711
if form.cleaned_data.get("users"):
17091712
filters.update(creator__in=form.cleaned_data["users"])
17101713

1714+
if not form.cleaned_data.get("include_bots"):
1715+
filters.update(creator__profile__is_bot=False)
1716+
17111717
start = form.cleaned_data.get("start")
17121718
end = form.cleaned_data.get("end")
17131719
if start or end:

0 commit comments

Comments
 (0)